Published on

섹션 6: Laser Defender 3

#Laser Defender - 3

Unity 2D 슈팅 게임의 완성도를 높이는 마지막 가이드입니다. 오디오 시스템, UI 설계, 점수 시스템 등을 구현합니다.

#📋 목차

  1. Unity 게임에 오디오 추가
  2. 배경 음악 시스템
  3. 점수 시스템 구현
  4. 에너지 시스템
  5. UI 설계
  6. 씬 관리 시스템
  7. 싱글턴 패턴 활용
  8. 게임 완성

#{ Unity 게임에 오디오 추가 }

이 강의에서는 Unity 게임에 오디오 효과를 추가하는 과정을 다룹니다. 발사음과 데미지 효과음을 구현

#1. 오디오 클립 준비

작업: 사용할 오디오 클립을 다운로드하고 프로젝트에 추가.

세부 과정:

		1. 오디오 소스를 찾기 위해 kenney.nl 같은 사이트에서 'sci-fi' 오디오 팩을 다운로드.

		2. 다운로드한 오디오 파일을 Unity 프로젝트의 [Assets pack] 폴더 내 sci-fi sounds 폴더에 추가.

개념:

		오디오 클립: 짧은 소리(예: 발사음, 폭발음)를 의미하며, Unity에서는 AudioClip 타입으로 관리.

		Unity에서 오디오를 재생하려면 AudioSource 컴포넌트나 AudioSource.PlayClipAtPoint 같은 메서드를 사용.

		짧은 오디오 클립은 런타임에 재생 후 자동으로 사라지도록 설정 가능.

#2. AudioPlayer 게임 오브젝트 및 스크립트 생성

작업: 오디오를 관리할 AudioPlayer 오브젝트와 스크립트를 생성.

세부 과정:

		1. Hierarchy에서 우클릭 → Create Empty로 새 게임 오브젝트 생성 → 이름: AudioPlayer.

		2. Transform 리셋으로 위치 초기화.

		3. [Scripts] 폴더에서 새 C# 스크립트 생성 → 이름: AudioPlayer.

		4. AudioPlayer 오브젝트에 스크립트를 추가.

개념:

		게임 오브젝트: Unity에서 오디오를 재생하려면 오브젝트에 스크립트를 붙여 관리.

		Transform 리셋: 오브젝트의 위치, 회전, 크기를 기본값(0, 0, 0)으로 설정해 깔끔하게 관리.

#3. AudioPlayer 스크립트 작성

작업: 발사음과 데미지음을 재생할 수 있도록 AudioPlayer 스크립트 작성.

#1. 변수 선언:

[Header("Shooting")]
[SerializeField] private AudioClip shootClip;
[SerializeField][Range(0f, 1f)] float shootVolume = 1f;

[Header("Damage")]
[SerializeField] private AudioClip damageClip;
[SerializeField][Range(0f, 1f)] float damageVolume = 1f;
[Header]: Inspector에서 변수를 그룹화해 가독성 향상.

[SerializeField]: private 변수를 Inspector에서 수정 가능하도록 설정.

[Range(0f, 1f)]: 볼륨을 0~1 사이로 제한하며 슬라이더 제공.

#2. 공통 재생 메서드

private void PlayClip(AudioClip clip, float volume)
{
    if (clip != null)
    {
        Vector3 cameraPos = Camera.main.transform.position;
        AudioSource.PlayClipAtPoint(clip, cameraPos, volume);
    }
}
AudioSource.PlayClipAtPoint: 오디오 클립을 특정 위치(카메라 위치)에서 재생 후 자동 소멸.

#3. 발사음 재생 메서드

public void PlayShootingClip()
{
    if (shootClip != null)
    {
        PlayClip(shootClip, shootVolume);
    }
}

#4. 데미지음 재생 메서드

public void PlayDamageClip()
{
    if (damageClip != null)
    {
        PlayClip(damageClip, damageVolume);
    }
}
AudioSource.PlayClipAtPoint: 짧은 오디오 클립을 재생할 때 유용. 클립, 위치, 볼륨을 인자로 받음.

코드 재사용: PlayClip 메서드를 만들어 중복 코드 줄이고 유지보수성 향상.

#4. Shooter 스크립트 수정 (발사음 추가)

작업: Shooter 스크립트에서 발사 시 오디오 재생.

#1. AudioPlayer 참조 추가

AudioPlayer audioPlayer;

void Awake()
{
    audioPlayer = FindAnyObjectByType<AudioPlayer>();
}
FindAnyObjectByType: 씬에서 AudioPlayer 컴포넌트를 찾아 참조.

#2. FireContinuously 코루틴에서 발사음 재생

    IEnumerator FireContinuously()
    {
        while (true)
        {
            GameObject instance = Instantiate(projectilePrefab, transform.position, Quaternion.identity);
            Rigidbody2D rb = instance.GetComponent<Rigidbody2D>();
            if (rb != null)
            {
                rb.velocity = transform.up * projectileSpeed;
            }
            Destroy(instance, projectileLifetime);
            float timeToNextProjectile = Random.Range(baseFiringRate - firingRateVariance, baseFiringRate + firingRateVariance);
            timeToNextProjectile = Mathf.Clamp(timeToNextProjectile, minimumFiringRate, float.MaxValue);

            audioPlayer.PlayShootingClip(); // 발사음 재생

            yield return new WaitForSeconds(timeToNextProjectile);
        }
    }
발사체 생성 후 호출.
코루틴: FireContinuously는 반복적인 발사를 관리. yield return으로 시간 간격 조정.

FindAnyObjectByType: 단일 인스턴스 스크립트를 찾을 때 유용하지만, 여러 개일 경우 주의.

#5. Health 스크립트 수정 (데미지음 추가)

작업: Health 스크립트에서 데미지 발생 시 오디오 재생.

#1. AudioPlayer 참조 추가:

AudioPlayer audioPlayer;

void Awake()
{
    cameraShake = Camera.main.GetComponent<CameraShake>();
    audioPlayer = FindAnyObjectByType<AudioPlayer>();
}

#2. OnTriggerEnter2D에서 데미지음 재생

    void OnTriggerEnter2D(Collider2D collision)
    {
        DamageDealer damageDealer = collision.gameObject.GetComponent<DamageDealer>();

        if (damageDealer != null)
        {
            TakeDamage(damageDealer.GetDamage());
            PlayHitEffect();
            audioPlayer.PlayDamageClip(); // 데미지 처리 후 호출.
            ShakeCamera();
            damageDealer.Hit();
        }
    }
데미지 처리 후 호출.
OnTriggerEnter2D: 2D 충돌 시 호출되는 Unity 이벤트.

데미지음은 데미지 발생 시점(TakeDamage 호출 후)에 재생.

#6. Unity에서 설정 및 테스트

작업: Inspector에서 오디오 클립 설정 및 테스트.

세부 과정:

1. AudioPlayer 오브젝트의 Inspector에서:

		Shoot Clip: 예: laserSmall_001.

		Damage Clip: 예: ExplosionCrunch_04.

		볼륨 슬라이더로 조정(기본값: 1f).

		Game 창에서 Mute Audio가 체크 해제되었는지 확인.

2. 플레이 테스트:

		발사 시 laserSmall_001 소리 확인.

		데미지 시 ExplosionCrunch_04 소리 확인.

4. 발사음이 너무 빠르면 Shooter 스크립트의 baseFiringRate를 조정(예: 0.4초).

개념:

		Inspector 설정: [SerializeField]로 선언한 변수는 Unity Inspector에서 설정 가능.

		Mute Audio: Game 창의 오디오 뮤트 설정을 확인하지 않으면 소리가 안 들릴 수 있음.

#7. 핵심 개념 요약

오디오 클립: 짧은 효과음은 AudioClip으로 관리, AudioSource.PlayClipAtPoint로 재생.

AudioPlayer: 오디오 재생을 중앙화해 관리, 코드 재사용성 향상.

Inspector 활용: [SerializeField]와 [Range]로 설정 편리성 제공.

스크립트 간 참조: FindAnyObjectByType으로 다른 스크립트 접근.

테스트 주의점: Game 창의 Mute Audio 확인, 발사 속도 조정으로 소리 겹침 방지.

#{ 게임에 배경 음악 추가하기 }

#1. 배경 음악 찾기

사이트: Opengameart.org에서 인디 개발자를 위한 무료 리소스를 찾음.

개념: CC0 라이선스는 저작권 제한 없이 자유롭게 사용할 수 있는 퍼블릭 도메인 음악을 의미. 게임 개발 시 저작권 문제 없이 사용 가능.

#2. 유니티에서 오디오 시스템 이해

유니티 오디오의 3요소:

		1. Audio Listener: 소리를 듣는 객체. 기본적으로 메인 카메라에 붙어 있음.

				예: Main Camera에 Audio Listener 컴포넌트가 있어 소리가 들림.

		2. Audio Source: 오디오를 재생하는 객체. 오디오 클립을 재생하도록 설정.

				예: AudioSource.PlayClipAtPoint는 일회성 소리 재생에 사용.

		3. Audio File: 실제 재생될 오디오 파일(예: MP3, WAV).


		흐름: Audio File → Audio Source → Audio Listener.

개념:

		Audio Listener는 게임 내에서 플레이어가 소리를 듣는 기준점.

		Audio Source는 소리의 위치, 볼륨, 루핑 여부 등을 제어.

		One-shot Audio는 짧게 한 번 재생되는 소리(예: 총소리), 배경 음악은 루핑으로 계속 재생.

#3. 배경 음악 추가하기

Audio Player 설정:

		1. Audio Player 게임 오브젝트에 새 컴포넌트로 Audio Source 추가.

		2. 설정:

				AudioClip: 원하는 음악 파일 드래그하여 할당.

				Play on Awake: 게임 시작 시 자동 재생(체크).

				Loop: 음악이 끝나면 반복 재생(체크).

				Volume: 볼륨 조절(0~1 슬라이더).

				Spatial Blend: 2D로 설정(3D는 소리 위치에 따라 볼륨 변화, 2D는 균일).

				3D Sound Settings, Output, Bypass Effects 등은 2D 게임에서 무시.


		3. 결과: 게임 시작 시 배경 음악이 루핑되며 재생.


		개념:

		Audio Source 컴포넌트는 오디오 재생의 핵심. Play on Awake와 Loop 옵션은 배경 음악에 필수.

		2D vs 3D 사운드: 2D는 위치 상관없이 균일한 소리, 3D는 거리/방향에 따라 소리 크기 변화.
Unity 게임 스크린샷

#{ 점수 및 에너지 시스템 구현 }

#목표

1. Health 스크립트에 플레이어의 현재 체력(에너지)을 반환하는 public 게터 메서드를 추가.

2. ScoreKeeper 스크립트를 작성해 점수를 관리:

		private 점수 변수.

		점수를 반환하는 public 게터 메서드.

		점수를 수정하는 public 메서드.

		점수를 초기화하는 public 메서드.

3. 적이 파괴될 때 점수가 증가하도록 Health 스크립트 수정.

4. Unity에서 이를 테스트하고 UI로 점수/체력을 표시할 준비.

#1. Health 스크립트 수정

Health.cs는 플레이어와 적의 체력을 관리하며, 적이 죽을 때 점수를 추가하는 역할을 합니다.

#1.1. public 게터 메서드 추가

목표: 플레이어의 현재 체력을 반환하는 메서드 추가.

작업:

		Health 스크립트에 public int GetHealth() 메서드를 추가.

		이 메서드는 private int health 값을 반환.
public int GetHealth()
{
    return health;
}
개념: 게터 메서드는 private 변수의 값을 외부에서 안전하게 읽을 수 있도록 해줍니다. UI에서 체력을 표시하려면 이 메서드가 필요합니다.

#1.2. 플레이어와 적 구분

문제: 플레이어와 적이 같은 Health 스크립트를 사용하므로, 플레이어가 죽을 때 점수가 추가되지 않도록 해야 함.

작업:

		[SerializeField] bool isPlayer 변수를 추가해 플레이어인지 적인지 구분.

		Unity Inspector에서 플레이어 오브젝트는 isPlayer를 체크, 적은 체크 해제.
[SerializeField] bool isPlayer = false;
개념: [SerializeField]는 private 변수를 Unity Inspector에서 수정 가능하게 만듭니다.

이를 통해 동일한 스크립트를 플레이어와 적에 다르게 설정할 수 있습니다.

#1.3. 점수 변수 추가

목표: 적이 죽을 때 추가할 점수를 설정.

작업:

		[SerializeField] int score = 50; 변수를 추가.

		이 값은 적이 파괴될 때 ScoreKeeper에 추가될 점수.
[SerializeField] int score = 50;
개념: 적마다 다른 점수를 설정할 수 있어 게임 디자인의 유연성을 높입니다. 예: 강한 적은 높은 점수, 약한 적은 낮은 점수.

#1.4. 점수 추가 로직

목표: 적이 죽을 때 점수를 추가.

작업:

		ScoreKeeper를 참조하도록 Health 스크립트에 변수 추가.

		Awake에서 FindAnyObjectByType<ScoreKeeper>()로 ScoreKeeper를 찾음.

		Die 메서드에서 isPlayer가 false인 경우(즉, 적인 경우) ScoreKeeper.ModifyScore(score) 호출.
ScoreKeeper scoreKeeper;

void Awake()
{
    scoreKeeper = FindAnyObjectByType<ScoreKeeper>();
    // ... 기존 코드 ...
}

void Die()
{
    if (!isPlayer)
    {
        scoreKeeper.ModifyScore(score);
    }
    Destroy(gameObject);
}
개념: FindAnyObjectByType는 씬에서 특정 컴포넌트를 찾아 참조합니다.

Die 메서드는 객체가 파괴되기 전에 점수를 추가해 게임 로직을 유지합니다.

#2. ScoreKeeper 스크립트 작성

ScoreKeeper.cs는 점수를 관리하는 스크립트로, 점수를 저장하고 수정하며 UI로 내보낼 준비를 합니다.

#2.1. 점수 변수와 게터 메서드

작업:

		private int score = 0; 변수로 점수를 저장.

		public int GetScore() 메서드로 점수 반환.
private int score = 0;

public int GetScore()
{
    return score;
}
개념: private 변수는 외부에서 직접 수정되지 않도록 보호하며,

public 게터 메서드는 UI나 다른 스크립트에서 점수를 읽을 수 있게 합니다.

#2.2. 점수 수정 메서드

작업:

		public void ModifyScore(int value) 메서드로 점수 추가.

		Mathf.Clamp로 점수가 0 아래로 내려가지 않도록 제한.

		디버깅을 위해 Debug.Log 추가.
public void ModifyScore(int value)
{
    score += value;
    score = Mathf.Clamp(score, 0, int.MaxValue);
    Debug.Log("Score updated: " + score);
}
개념: Mathf.Clamp는 값을 최소값(0)과 최대값(int.MaxValue) 사이로 제한합니다.

Debug.Log는 콘솔에 로그를 출력해 디버깅에 유용합니다.

#2.3. 점수 초기화 메서드

작업:

		public void ResetScore() 메서드로 점수를 0으로 초기화.
public void ResetScore()
{
    score = 0;
}
개념: 초기화 메서드는 게임 재시작 시 점수를 리셋하는 데 사용됩니다.

#3. Unity 설정

작업:

1. ScoreKeeper 오브젝트 생성:

		Hierarchy에서 빈 게임 오브젝트 생성 후 이름 ScoreKeeper로 설정.

		ScoreKeeper 스크립트를 추가.

2. 플레이어 설정:

		플레이어 오브젝트의 Health 컴포넌트에서 Is Player 체크.

		Score 값을 0으로 설정(플레이어는 점수를 주지 않음).

3. 적 설정:

		적 오브젝트의 Health 컴포넌트에서 Is Player 체크 해제.

		Score 값을 50(또는 원하는 값)으로 설정.

4. 테스트:

		게임 실행 후 적을 파괴해 콘솔에서 Debug.Log로 점수 증가 확인.


개념: Unity의 Inspector를 통해 스크립트의 [SerializeField] 변수를 설정하면,

동일한 스크립트를 여러 오브젝트에 다르게 적용할 수 있습니다.

#요약

1. Health 스크립트:

		GetHealth()로 체력 반환.

		isPlayer로 플레이어/적 구분.

		score 변수로 적 파괴 시 점수 설정.

		Die 메서드에서 적 파괴 시 점수 추가.

2. ScoreKeeper 스크립트:

		score 저장, GetScore로 반환.

		ModifyScore로 점수 수정(0 이하 방지).

		ResetScore로 점수 초기화.

3. Unity 설정:

		ScoreKeeper 오브젝트 생성.

		플레이어와 적의 Health 컴포넌트 설정.

		콘솔로 점수 증가 테스트.

#{ UI 설계 및 구현 }

#1. UI 설계의 개요

목표:

		게임의 핵심 정보(플레이어 에너지, 점수 등)를 플레이어에게 효과적으로 전달하는 UI를 설계하고 구현.

UI의 역할:

		게임 정보를 직관적으로 보여주되, 게임 플레이를 방해하지 않도록 적절히 배치.

고려사항:

		플레이어 시선: 플레이어는 주로 화면 중앙(적군, 플레이어 비행선)에 집중.

		UI 배치: 시선을 방해하지 않도록 화면 하단에 UI 배치 권장.

		확장 가능성: 기본적으로 에너지(Health)와 점수(Score)를 표시하지만, 필요 시 추가 요소(예: 웨이브 정보, 에너지 보충제 상태, 플레이 시간 등) 고려.

개념 설명:

		UI는 직관성과 비침투성(플레이를 방해하지 않음)을 균형 있게 설계해야 합니다.

		플레이어가 자주 보는 화면 영역을 분석해 UI를 방해가 적은 위치에 배치하는 것이 중요합니다.

#2. UI 요소 선택

게임 UI에 포함할 요소를 결정합니다. 강의에서는 다음과 같은 요소를 선택:

		에너지(Health): 플레이어의 현재 에너지 상태를 표시.

				옵션:

						슬라이더(간단하고 직관적)

						하트 컨테이너(시각적 표현)

						퍼센트 텍스트(간단한 숫자 표시)

						스프라이트 주변 아이콘/데미지 효과(직관적이지만 UI와 직접 관련 없음)

				결정: 슬라이더 사용(단순하고 연습에 적합).


			점수(Score): 게임 진행 상황을 나타내는 점수 표시.

				옵션: 텍스트로 표시(점수는 숫자로 표현하는 것이 일반적).


		확장 가능 요소 (선택 사항):

				현재/다음 웨이브 정보.

				에너지 보충제 상태.

				플레이 시간(무한 진행 게임의 경우).

개념 설명:

		UI 요소는 게임의 핵심 정보를 반영해야 하며,

		플레이어가 한눈에 이해할 수 있도록 단순하고 명확한 형태를 선택하는 것이 좋습니다.

		슬라이더는 연속적인 값을 시각적으로 보여주기에 적합하고, 텍스트는 숫자 정보를 명확히 전달합니다.

#3. 유니티에서 UI 구현

#3.1. 캔버스 추가

작업:

		Hierarchy에서 우측 클릭 → UI → Canvas 추가.

		캔버스 추가 시 EventSystem 자동 생성.

		경고 해결: EventSystem에서 경고(구형 입력 시스템 사용) 발생 시, Replace 버튼 클릭해 최신 입력 시스템으로 전환.

설명:

		Canvas: 모든 UI 요소의 부모 객체. UI의 배치와 스케일링을 관리.

				역할: 화면 크기에 따라 UI를 렌더링하고, UI 요소의 계층 구조를 정의.

				특징: 캔버스는 2D 공간에서 UI를 배치하며, 3D 게임 월드와 별개로 동작.


		EventSystem: UI 상호작용(버튼 클릭, 슬라이더 드래그 등)을 처리.

				구형 vs 최신 입력 시스템: 유니티의 최신 입력 시스템(Input System 패키지)은 더 유연하고 다양한 입력 장치를 지원.

				Replace는 구형 시스템(Input Manager)을 최신 시스템으로 교체.

개념 설명:

		Canvas의 중요성: 캔버스는 UI의 "캔버스"로, 모든 UI 요소를 포함하는 컨테이너. 반응형 UI를 위해 캔버스 설정이 핵심.

		EventSystem의 역할: UI와 플레이어 입력 간의 연결을 담당. 최신 입력 시스템은 멀티플랫폼 지원과 커스터마이징이 용이.

#3.2. 캔버스 설정 (Canvas Scaler)

작업:

		Canvas 선택 → Canvas Scaler 컴포넌트 수정.

		UI Scale Mode:

				기본값: Constant Pixel Size (고정 픽셀 크기).

				변경: Scale With Screen Size (화면 크기에 비례).

		Reference Resolution: 1080x1920 (9:16 비율, HD 화질) 설정.

설명:

		Constant Pixel Size: UI 요소의 픽셀 크기가 고정. 다양한 해상도에서 UI 크기가 일정하지 않을 수 있음.

				단점: 작은 화면에서는 UI가 너무 크게 보이고, 큰 화면에서는 작게 보임.

		Scale With Screen Size: UI 요소가 화면 해상도에 비례해 크기 조정.

				장점: 모든 화면 크기에서 UI 비율이 일정. 반응형 디자인에 적합.

		Reference Resolution: UI 설계의 기준 해상도. 1080x1920은 모바일 게임에 적합한 표준 HD 해상도.

결과: UI 요소가 화면 크기에 따라 동적으로 조정되어 일관된 비율 유지.

개념 설명:

		반응형 UI: 다양한 디바이스(모바일, PC, 태블릿)에서 UI가 적절히 보이도록 스케일링하는 것.

		Scale With Screen Size는 이를 구현하는 표준 방법.


		Reference Resolution: UI 레이아웃을 설계할 때 기준이 되는 해상도.

		실제 화면 해상도와 달라도 비율을 유지하며 스케일링.

#3.3. UI 패널 추가

작업:

		캔버스 내에 UI → Panel 추가.

		크기 조정: 화면 하단 7% 차지하도록 설정.

				Anchor Presets로 앵커를 하단으로 설정.

				RectTransform에서 Top을 0으로 설정해 하단 정렬.

				Game View에서 리사이즈 테스트해 비율 유지 확인.

설명:

		Panel: UI 요소를 그룹화하는 컨테이너. 배경 이미지 역할도 수행.

				용도: 슬라이더, 텍스트 등 여러 UI 요소를 한 곳에 정리.

				시각적 역할: 색상이나 투명도로 UI의 시각적 계층을 구분.


		Anchor: UI 요소의 상대적 위치를 고정하는 기준점.

				역할: 화면 크기가 변해도 UI 요소가 지정된 위치(예: 하단)에 유지되도록 함.

				설정 방법: Anchor Presets로 미리 정의된 위치 선택(예: 하단 중앙)하거나, 수동으로 앵커 좌표 입력.


		RectTransform: UI 요소의 위치, 크기, 회전을 정의하는 컴포넌트.

				Top=0: 패널이 화면 하단에 붙도록 설정.

				비율 유지: 화면 크기 변화에도 패널이 7% 비율을 유지.

개념 설명:

			패널의 역할: UI 요소를 그룹화해 관리 편의성과 시각적 일관성을 제공. 예: 배경 색상으로 UI 영역을 구분.

			앵커의 중요성: 반응형 UI의 핵심. 앵커는 UI 요소가 화면의 특정 위치(상단, 하단, 중앙 등)에

									  상대적으로 고정되도록 함. 예: 하단 앵커는 UI를 항상 화면 하단에 유지.

			RectTransform: 일반 3D 객체의 Transform과 달리, 2D UI를 위한 위치/크기 제어 도구. 앵커와 함께 사용해 반응형 배치 구현.

#3.4. 에너지 슬라이더 추가 (HealthSlider)

작업:

패널 내에 UI → Slider 추가, 이름 HealthSlider.

앵커 설정: 패널 높이에 맞게, 좌우 패딩(Left: 25, Right: 25) 추가.

설정:

		Interactable 끄기: 플레이어가 슬라이더를 조작하지 못하도록.

		Transition을 None으로: 슬라이더 이동 시 애니메이션 효과 제거.

		Value를 0.5로: 테스트용 초기 값 설정.

		Handle 비활성화/삭제: 플레이어가 조작하지 않으므로 불필요.

설명:

		Slider: 연속적인 값을 시각적으로 표현(예: 0~100% 에너지).

				구성 요소:

						Background: 슬라이더의 배경 바.

						Fill Area: 현재 값을 채우는 색상 바.

						Handle: 슬라이더를 드래그하는 핸들(비활성화 가능).

				Interactable: 슬라이더의 상호작용 여부. 끄면 플레이어가 슬라이더를 움직일 수 없음.

				Transition: 슬라이더 값 변경 시 애니메이션 효과. None으로 설정하면 즉시 반영.


		패딩: 슬라이더가 패널 경계에 너무 붙지 않도록 여백 추가.

개념 설명:

		슬라이더의 구조: 슬라이더는 Background, Fill Area, Handle로 구성. Fill Area는 값에 따라 동적으로 채워짐.

		비상호작용 UI: 게임 상태 표시용 UI는 플레이어 조작을 막아야 함. Interactable을 끄면 슬라이더는 표시 전용이 됨.

		패딩의 역할: UI 요소 간 여백은 가독성과 시각적 깔끔함을 높임.

#3.5. 점수 텍스트 추가

작업:

		패널 내에 UI → Text - TextMeshPro 추가.

		TMP Essentials 임포트: TextMeshPro 사용 전 필수 리소스 임포트 (Window → TextMeshPro → Import TMP Essential Resources).

		앵커 설정: 패널 중앙, 좌우 패딩(Left: 25, Right: 25), Top: 0, Bottom: 0.

		텍스트 설정:

				Auto Size 활성화: 텍스트 크기가 화면 크기에 따라 조정.

				폰트 크기: 최소 5, 최대 72.

				정렬: 오른쪽, 가운데.

				테스트용 숫자 입력(예: "1234").

설명:

		TextMeshPro: 유니티의 기본 텍스트보다 고급 렌더링과 스타일링 제공.

				장점: 선명한 텍스트, 아웃라인, 그림자, 글로우 등 다양한 효과 지원.

				TMP Essentials: TextMeshPro 사용에 필요한 기본 폰트와 리소스.


		Auto Size: 텍스트 크기를 화면 해상도에 맞게 자동 조정.

				최소/최대 폰트 크기: 텍스트가 너무 작거나 커지지 않도록 제한.


		정렬: 텍스트의 수평/수직 위치를 조정해 가독성 향상.

개념 설명:

		TextMeshPro의 장점: 고해상도 텍스트 렌더링과 다양한 스타일링 옵션(아웃라인, 그림자 등)으로 UI의 시각적 품질 향상.

		Auto Size: 반응형 UI에서 텍스트 크기를 동적으로 조정해 모든 화면에서 가독성 유지.

		패딩과 정렬: UI 요소의 깔끔한 배치를 위해 여백과 정렬은 필수.

#**3.6. UI 디자인 개선**

작업:

		패널:

				Image 컴포넌트의 Color 변경(예: 파란색/보라색 톤, 불투명도 조정).


		HealthSlider:

				Background: 보라색, 불투명도 100%.

				Fill Area: 파란색.


		TextMeshPro:

				폰트 변경: KenneySpaceFont 사용.

						Font Asset 생성: Window → TextMeshPro → Font Asset Creator → 폰트 선택 → Generate Font Atlas.

						적용: 생성된 Font Asset을 TextMeshPro에 드래그.

				색상: 슬라이더와 조화로운 파란색/보라색, 불투명도 조정.

				아웃라인: 두께 0.25, 불투명도 100.

				글로우 효과: 텍스트를 돋보이게 함.

설명:

		색상: UI의 색상은 게임 분위기와 일관성을 유지하며 가독성을 높임.

				예: 파란색/보라색은 미래적/우주적 테마에 적합.

		Font Asset: TextMeshPro에서 커스텀 폰트를 사용하려면 폰트를 Font Asset으로 변환해야 함.

				Font Atlas: 폰트의 텍스처를 생성해 렌더링 최적화.

		아웃라인/글로우: 텍스트를 배경과 구분해 가독성을 높이고 시각적 매력을 추가.

개념 설명:

		색상 심리학: 색상은 게임의 분위기를 강화. 예: 파란색은 차분하고 신뢰감, 보라색은 신비로움 전달.

		Font Asset의 필요성: TextMeshPro는 커스텀 폰트를 고품질로 렌더링하기 위해 Font Atlas를 생성. 이는 텍스트의 선명도와 효과 적용을 보장.

		시각적 효과: 아웃라인과 글로우는 텍스트를 배경에서 돋보이게 하여 가독성과 미적 품질을 동시에 충족.
Unity 게임 스크린샷 Unity 게임 스크린샷 Unity 게임 스크린샷 Unity 게임 스크린샷
패널 추가
Unity 게임 스크린샷
엥커 조절
Unity 게임 스크린샷
슬라이드 바 앵커를 조절 ( 앵커는 x 표시로 4개가 분할된 형태 )
Unity 게임 스크린샷 Unity 게임 스크린샷
텍스트 앵커 설정과 25 정도의 패팅 그리고 글자 크기 auto
Unity 게임 스크린샷
폰트 추가
Unity 게임 스크린샷
폰트 에셋 만들기

#{ UI 연결 및 구현 }

이 강의에서는 Unity에서 게임 UI를 구현하고, 이를 게임 로직과 연결하는 과정을 다룹니다.

UIDisplay 스크립트를 만들어 캔버스에 첨부하고,

플레이어의 체력(Health)과 점수(Score)를 UI에 실시간으로 반영하는 방법을 배웁니다.

#1. 목표

목적:

		UIDisplay 스크립트를 작성하여 UI 요소(체력 슬라이더, 점수 텍스트)를 게임 로직(Health, ScoreKeeper)과 연결해 실시간으로 업데이트.

결과:

		플레이어의 체력과 점수가 UI에 반영되고, 점수 형식을 아케이드 스타일로 포맷팅.

#2. 사전 준비

필요한 컴포넌트:

		Health: 플레이어의 체력 정보를 관리하는 스크립트.

		ScoreKeeper: 게임 점수를 관리하는 스크립트.

		Canvas: UI 요소(체력 슬라이더, 점수 텍스트)를 포함하는 Unity의 UI 컨테이너.

UI 요소:

		Slider: 플레이어의 체력을 시각적으로 표시.

		TextMeshProUGUI: 점수를 텍스트로 표시.

#3. 구현 단계

#3.1. UIDisplay 스크립트 생성

1. 스크립트 생성:

		Unity 프로젝트의 [Scripts] 폴더에서 새 C# 스크립트를 생성하고, 이름을 UIDisplay로 지정.

		스크립트를 캔버스 오브젝트에 추가(드래그 앤 드롭).

2. 필요한 네임스페이스 추가:

		UI와 TextMeshPro를 사용하기 위해 아래 두 줄을 스크립트 상단에 추가:
using UnityEngine.UI; // Slider 등 UI 컴포넌트 사용
using TMPro; // TextMeshPro 텍스트 사용
개념: 네임스페이스는 관련 기능(클래스, 메소드 등)을 묶는 코드 그룹입니다.

UnityEngine.UI는 Unity의 기본 UI 시스템, TMPro는 TextMeshPro를 다루기 위해 필요합니다.

#3.2. 변수 선언

변수는 UI와 게임 데이터를 연결하기 위해 사용됩니다. [SerializeField]를 사용해 Unity Inspector에서 수동으로 연결 가능.

두 개의 헤더로 구분:
[Header("Health")]
[SerializeField] Slider healthSlider; // 체력 슬라이더
[SerializeField] Health playerHealth; // 플레이어 체력 스크립트

[Header("Score")]
[SerializeField] TextMeshProUGUI scoreText; // 점수 텍스트
ScoreKeeper scoreKeeper; // 점수 관리 스크립트
개념:

		[SerializeField]: private 변수라도 Unity Inspector에서 수정 가능하도록 설정.

		Slider: Unity의 UI 컴포넌트로, 체력 바 같은 비율을 시각적으로 표시.

		TextMeshProUGUI: 고급 텍스트 렌더링 컴포넌트로, 점수 표시용.

		ScoreKeeper는 Scene에 단 하나만 존재하므로, FindObjectOfType으로 동적으로 가져옴.

#3.3. Awake에서 ScoreKeeper 연결

ScoreKeeper는 수동 연결 대신 코드로 찾아옴:
void Awake()
{
    scoreKeeper = FindObjectOfType<ScoreKeeper>();
}
개념: Start는 오브젝트 초기화 직후 호출. 여기서는 슬라이더의 maxValue를 플레이어의 초기 체력으로 설정해

UI가 게임 데이터와 동기화되도록 함. 이렇게 하면 체력 값이 바뀌어도 UI가 자동으로 반영.

#3.5. Update에서 UI 업데이트

매 프레임 UI를 갱신:
void Update()
{
    healthSlider.value = playerHealth.GetHealth(); // 체력 슬라이더 업데이트
    scoreText.text = scoreKeeper.GetScore().ToString("00000"); // 점수 텍스트 업데이트
}
개념:

		Update: 매 프레임 호출되는 Unity 메소드로, 실시간 데이터 갱신에 적합.

		healthSlider.value: 슬라이더의 현재 값을 플레이어 체력으로 설정.

		ToString("00000"): 점수를 5자리 숫자로 포맷팅(예: 00042). 이는 아케이드 스타일의 고정된 자릿수 표시를 위해 사용.

#3.6. Unity에서 연결

1. Inspector에서 연결:

		캔버스 오브젝트의 UIDisplay 컴포넌트에 다음을 드래그 앤 드롭:

				HealthSlider: 캔버스 내의 슬라이더 오브젝트.

				PlayerHealth: 플레이어 오브젝트의 Health 컴포넌트.

				ScoreText: 캔버스 내의 TextMeshPro 텍스트 오브젝트.

		텍스트 오브젝트의 이름을 Text (TMP)에서 ScoreText로 변경해 가독성 향상.

2. 테스트:

		게임을 실행(Play)하여 체력 슬라이더와 점수 텍스트가 실시간으로 업데이트되는지 확인.

		적을 처치하면 점수가 올라가고, 체력이 감소하면 슬라이더가 반영됨.

#3.7. 점수 텍스트 포맷 개선

초기 점수 표시(예: 42)에 빈칸이 많아 보기 좋지 않음.

ToString("00000")을 사용해 점수를 5자리 고정 포맷(예: 00042)으로 변경.

결과: 점수 텍스트가 아케이드 스타일 LCD처럼 보임.

개념: ToString의 포맷 문자열(예: "00000")은 숫자를 고정 자릿수로 표시. 0은 빈 자리를 0으로 채움. 원한다면 "D5"처럼 다른 포맷도 사용 가능.

#ToString이란?

정의: 모든 C# 객체는 기본적으로 ToString 메서드를 가지고 있으며, 이를 호출하면 객체를 문자열 형태로 표현합니다.

용도: 숫자(int, float 등)를 UI 텍스트에 표시하거나, 로그 출력, 데이터 포맷팅 등에 사용.

기본 동작: 파라미터 없이 호출하면 객체의 기본 문자열 표현을 반환. 예를 들어, int 값은 숫자를 그대로 문자열로 변환.
Unity 게임 스크린샷
인스펙터에 다 이어서 넣어준다.

특이한 점은 Health 스크립트는 플레이어를 끌어와서 넣어줘야 한다 ( 당연한거긴 한데 )

#Unity의 Inspector는 컴포넌트 자체를 직접 드래그할 수 없다

playerHealth는 Health 타입의 컴포넌트를 참조합니다.

Unity의 Inspector는 컴포넌트 자체를 직접 드래그할 수 없고, 그 컴포넌트가 붙어 있는 게임 오브젝트를 드래그해야 합니다.

이유: Unity는 컴포넌트가 어떤 오브젝트에 붙어 있는지 알아야 그 컴포넌트의 데이터를 참조할 수 있음.

예시:

		Player 오브젝트에 Health 컴포넌트가 붙어 있다고 가정.

		Inspector에서 playerHealth 필드에 Player 오브젝트를 드래그하면, Unity가 자동으로 그 오브젝트의 Health 컴포넌트를 찾아 연결.

		만약 Health 스크립트 파일(프로젝트 창의 C# 파일)을 드래그하려고 하면,

		Unity는 이를 인식하지 못함. 왜냐하면 스크립트 파일은 코드 템플릿일 뿐, 실제 데이터를 가진 컴포넌트가 아님.

#별도의 Scene ( 메인메뉴, 게임오버 ) 생성

#강의 흐름 요약

목표: 게임 시작 시 메인 메뉴와 게임 종료 시 게임 오버 화면을 구현.

주요 작업:

		메인 메뉴와 게임 오버 화면을 각각 별도의 Scene으로 생성.

		UI 요소(버튼, 텍스트 등)를 설계하고, 프리팹(재사용 가능한 오브젝트)을 활용.

		점수 표시 및 버튼 동작(시작, 종료, 재시작, 메인 메뉴로 이동) 구현 준비.

		다음 강의: Scene 간 전환 및 버튼 기능 연결.

#단계별 작업 정리

#1. 프로젝트 준비

Scene 이름 변경:

		기본 Scene 이름(SampleScene)을 Game으로 변경해 명확히 함.

		Scene 이름은 프로젝트 구조를 이해하기 쉽게 설정하는 것이 중요.


		개념: Scene은 유니티에서 게임의 각 "화면" 또는 "레벨"을 나타내는 단위. 이름을 명확히 하면 팀 작업 및 유지보수에 유리.

재사용 가능한 프리팹 확인:

		Background: 배경 오브젝트를 메인 메뉴와 게임 오버 화면에서 재사용.

		AudioPlayer: 배경 음악 재생용 오브젝트, 모든 Scene에서 공통 사용.

		ScoreKeeper: 점수를 관리하는 오브젝트. 아직 점수 표시 기능은 미완성이나, 프리팹으로 저장해 재사용 가능.


		개념: 프리팹(Prefab)은 재사용 가능한 게임 오브젝트 템플릿. Scene 간 일관성을 유지하고 작업 효율성을 높임.

작업:

		Background와 AudioPlayer를 [Prefabs] 폴더로 드래그해 프리팹화.

		ScoreKeeper도 프리팹으로 저장, 점수 표시 기능은 나중에 구현.

#**2. 메인 메뉴 Scene 생성**

새 Scene 생성:

		[Scenes] 폴더에서 우클릭 > Create > Scene > 이름: MainMenu.

		저장 후 MainMenu Scene 열기.


		기본 설정:

		빈 Scene에 Background와 AudioPlayer 프리팹 추가.

		AudioPlayer의 음악은 UI 작업 중 방해가 될 수 있으니 일시적으로 비활성화.

		개념: Scene에 추가하는 오브젝트는 Hierarchy에 나타나며, 프리팹을 사용하면 동일한 설정을 여러 Scene에서 쉽게 적용 가능.

Canvas 추가:

		Hierarchy에서 우클릭 > UI > Canvas 추가.

		Canvas 추가 시 EventSystem이 자동 생성됨.

		EventSystem의 Standalone Input Module을 새로운 입력 시스템에 맞게 교체(Replace).


		개념: Canvas는 UI 요소를 배치하는 컨테이너. EventSystem은 버튼 클릭 등 사용자 입력을 처리.

Canvas 설정:

		Canvas Scaler를 Scale With Screen Size로 설정, Reference Resolution 설정(예: 1920x1080).


		개념: Canvas Scaler는 다양한 화면 크기에 UI를 적응시키는 도구.

		Reference Resolution은 기준 해상도를 설정해 UI 크기를 일정하게 유지.

#3. 메인 메뉴 UI 설계

버튼 추가:

		Canvas에서 우클릭 > UI > Button 추가.

		버튼의 Transition을 Color Tint로 설정.

		개념: Color Tint는 버튼의 상태(기본, 마우스 오버, 클릭 등)에 따라 색상을 변경해 사용자 피드백을 제공.

버튼 색상 설정:

		Normal Color: 기본 색상(예: 민트빛 초록).

		Highlighted Color: 마우스 오버 시 색상(예: 밝은 오렌지).

		Pressed Color: 클릭 시 색상(예: 생동감 있는 파란색).

		Selected Color: 선택 시 색상(예: 보라색).

		Disabled Color: 비활성화 시 색상(예: 반투명 회색).

		개념: 색상 피드백은 플레이어가 버튼 상태를 직관적으로 이해하도록 돕는 중요한 UI 요소.

텍스트 설정:

		제목과 자막을 위해 Text (TMP) 추가 (TextMeshPro 사용).

		새로운 Font Asset 생성 (예: KenneySpace 폰트 사용).

		제목: Horizontal Gradient로 투톤 보라색, Underlay로 3D 효과, Glow로 빛 효과.

		자막: main-kenney 폰트 재사용.

		개념: TextMeshPro는 고품질 텍스트 렌더링 도구. Font Asset은 폰트 스타일을 커스터마이징해 UI의 시각적 품질을 높임.

버튼 그룹화:

		빈 GameObject 생성, 이름: Button Group.

		Vertical Layout Group 컴포넌트 추가, Spacing을 25로 설정, Control Child Size 활성화.

		StartButton과 QuitButton을 추가.

		개념: Layout Group은 UI 요소를 자동으로 정렬해 수동 조정 작업을 줄임.

배경 최적화:

		Background의 Sprite Scroller 속도를 낮춰 메인 메뉴에서 혼란스럽지 않도록 조정.

#4. 게임 오버 Scene 생성

Scene 복제:

		MainMenu Scene을 복제(Ctrl+D) > 이름: GameOver.

		개념: Scene 복제는 유사한 구조를 가진 Scene을 빠르게 생성하는 방법.

UI 수정:

		TitleText를 "Game Over"로 변경.

		SubtitleText를 "Better Luck Next Time"으로 변경.

		점수 표시를 위해 Score Text 추가 (main-kenney 폰트 사용).

		버튼: Play Again과 Main Menu 버튼 추가.

		개념: 게임 오버 화면은 메인 메뉴와 유사하지만, 점수 표시와 버튼 기능이 다름.

배경 및 버튼:

		Background의 별 이동 속도는 메인 메뉴와 동일하게 느리게 유지.

		버튼 색상은 메인 메뉴와 동일한 스타일(마우스 오버: 보라색, 클릭: 파란색).
Unity 게임 스크린샷
새로 캔버스에 추가하면 이벤트 시스템에 리플레이스 눌러주기
Unity 게임 스크린샷
다양한 버튼 색상 옵션

#{ 레벨 매니저(LevelManager)\*\* 여러 씬 전환 }

#1. 빌드 설정(Build Settings)

1. 유니티 빌드 설정 열기:

		메뉴: File → Build Settings

		목적: 게임에 포함될 씬 목록을 확인하고 관리.

2. 씬 목록 확인:

		Scenes In Build에 기본 씬(예: SampleScene)이 표시됨.

		이 강의에서는 기본 씬 이름을 Game으로 변경.

3. 씬 추가 및 순서 조정:

		추가 씬(MainMenu, GameOver)을 Scenes In Build에 드래그하여 추가.

		유니티는 빌드 인덱스(Build Index)를 기준으로 씬을 로드하며, 인덱스 0이 게임 시작 시 첫 번째로 로드됨.

		MainMenu를 인덱스 0으로 설정하기 위해 드래그로 순서를 조정.

4. 설정 저장:

		설정 후 Build Settings 창을 닫음(빌드 버튼 누를 필요 없음).

개념: 빌드 인덱스와 씬 관리

		빌드 인덱스: 유니티에서 씬을 식별하는 숫자(0부터 시작). 게임 실행 시 첫 번째 씬(인덱스 0)부터 로드.

		씬 목록 관리: 모든 씬은 Build Settings에 추가되어야 게임에서 사용 가능.

		순서가 중요하며, 메인 메뉴가 첫 화면이 되도록 설정.

#2. 레벨 매니저(LevelManager) 생성

1. 게임 오브젝트 생성:

		Hierarchy에서 Create Empty로 빈 게임 오브젝트를 만들고 이름은 LevelManager로 지정.

		Transform Reset: 오브젝트의 위치, 회전, 크기를 초기화.

2. 스크립트 생성:

		[Scripts] 폴더에서 우클릭 → Create → C# Script → 이름: LevelManager.

		스크립트를 LevelManager 오브젝트에 드래그하여 컴포넌트로 추가.

3. 스크립트 작성:

		네임스페이스 추가: using UnityEngine.SceneManagement;를 추가하여 씬 관리 기능 사용.

		메소드 작성:

		LoadGame(): 게임 씬을 로드
public void LoadGame()
{
    SceneManager.LoadScene("Game"); // 씬 이름으로 로드
}

public void LoadMainMenu()
{
    SceneManager.LoadScene("MainMenu");
}

public void LoadGameOver()
{
    SceneManager.LoadScene("GameOver");
}

public void QuitGame()
{
    Debug.Log("Quit Game");
    Application.Quit(); // 게임 종료
}

#개념: SceneManager와 Application.Quit

주의: Application.Quit()은 WebGL 빌드에서 작동하지 않으며, 모바일에서는 추가 설정 필요.

개념: SceneManager와 Application.Quit

		SceneManager: 유니티에서 씬을 로드하거나 관리하는 클래스. LoadScene 메소드는 씬 이름(문자열) 또는 빌드 인덱스(숫자)로 씬을 로드.

		Application.Quit: 게임을 종료하는 명령. 단, WebGL이나 모바일 환경에서는 제한이 있음.

#3. 레벨 매니저를 프리팹(Prefab)으로 만들기

1. 프리팹 생성:

		LevelManager 오브젝트를 [Prefabs] 폴더로 드래그하여 프리팹으로 저장.

		왜 프리팹?: 모든 씬에서 동일한 LevelManager를 재사용하기 위함.

2. 버튼 연결:

		MainMenu 씬:

				StartButton의 On Click 이벤트에 LevelManager를 추가하고 LoadGame() 메소드 연결.

				QuitButton의 On Click 이벤트에 LevelManager를 추가하고 QuitGame() 메소드 연결.

		테스트: Play 버튼으로 게임 시작 및 종료 확인.

개념: 프리팹과 UI 이벤트

		프리팹: 재사용 가능한 게임 오브젝트 템플릿. 모든 씬에서 동일한 설정을 유지.

		On Click 이벤트: 유니티 UI 버튼에서 특정 동작(예: 메소드 호출)을 트리거하는 기능.

#4. Health 스크립트와 LevelManager 연동

#Health 스크립트 수정

LevelManager 참조 추가:
LevelManager levelManager;
void Awake()
{
    levelManager = FindObjectOfType<LevelManager>();
}
Die() 메소드에서 플레이어 사망 시 LoadGameOver() 호출
void Die()
{
    if (!isPlayer)
    {
        scoreKeeper.ModifyScore(score);
    }
    else
    {
        levelManager.LoadGameOver();
        Debug.Log("Player has died");
    }
    Destroy(gameObject);
}

#Game 씬에 LevelManager 추가:

LevelManager 프리팹을 Game 씬에 추가.
개념: FindObjectOfType

FindObjectOfType: 씬에서 특정 컴포넌트(예: LevelManager)를 찾아 참조. 성능상 주의 필요(빈번한 사용은 피해야 함).

#5. GameOver 씬 버튼 연결

1. GameOver 씬 설정:

		LevelManager 프리팹을 GameOver 씬에 추가.

		ReplayButton의 On Click 이벤트에 LevelManager → LoadGame() 연결.

		MainMenuButton의 On Click 이벤트에 LevelManager → LoadMainMenu() 연결.

2. 테스트:

		게임 플레이 → 사망 → GameOver 씬 전환 → 버튼으로 재시작 또는 메인 메뉴 이동 확인.

		개념: 씬 간 전환

		버튼과 LevelManager를 통해 씬 간 부드러운 전환 구현. 각 버튼은 특정 씬을 로드하도록 설정.

#6. 게임 오버 지연 로딩

코루틴 추가:

		LevelManager에 지연 로딩을 위한 코루틴 작성
[SerializeField] float sceneDelay = 2f;

IEnumerator WaitAndLoad(string sceneName, float delay)
{
    yield return new WaitForSeconds(delay);
    SceneManager.LoadScene(sceneName);
}
LoadGameOver() 수정:
public void LoadGameOver()
{
    StartCoroutine(WaitAndLoad("GameOver", sceneDelay));
}
2. SerializeField 사용:

		sceneDelay를 인스펙터에서 조정 가능하도록 설정.

		기본값: 2초.

3. 프리팹 업데이트:

		LevelManager 프리팹에서 sceneDelay 값을 조정.

4. 테스트:

		플레이어 사망 후 2초 지연 후 GameOver 씬으로 전환 확인.

개념: 코루틴과 SerializeField

		코루틴: 유니티에서 비동기 작업(예: 지연)을 처리하는 강력한 도구. IEnumerator와 yield return 사용.

		SerializeField: private 변수를 유니티 인스펙터에 노출하여 값을 쉽게 조정 가능.
Unity 게임 스크린샷
빌드 셋팅에 새로 만든 씬 추가
Unity 게임 스크린샷
LevelManager 오브젝트 ( 스크립트 말고 ) 를 버튼에 이어주고 버튼에 함수를 연결되게 해준다 ( ex. LoadGame )

#{ 싱글턴 패턴을 사용한 AudioPlayer 구현 }

이 강의에서는 Unity 게임에서 AudioPlayer가 씬(Scene) 전환 시 음악이 끊기는 문제를 해결하기 위해

**싱글턴 패턴(Singleton Pattern)**을 적용하는 방법을 배웁니다.

#1. 문제 상황

문제: 메인 메뉴에서 게임 씬으로 전환할 때, 기존 씬의 오브젝트(예: AudioPlayer)가 파괴되고

새 AudioPlayer가 생성되면서 음악이 처음부터 재생됨.

해결책: AudioPlayer를 씬 전환 시에도 유지되도록 싱글턴 패턴을 사용해 단일 인스턴스로 관리.

#2. 싱글턴 패턴이란?

정의: 클래스의 인스턴스를 단 하나로 제한하는 소프트웨어 디자인 패턴.

특징:

		특정 클래스의 객체가 게임 전체에서 하나만 존재하도록 보장.

		주로 글로벌 접근이 필요한 경우 사용 (예: 오디오 관리, 게임 매니저).

주의점:

		싱글턴은 안티 패턴으로 간주될 수 있음.

		과도한 글로벌 변수 사용은 코드 복잡성을 증가시키고 디버깅을 어렵게 만듦.

		Unity에서는 신중히 사용해야 함.

#3. 싱글턴 패턴 구현 방법

강의에서는 AudioPlayer를 싱글턴으로 만드는 두 가지 방법을 소개합니다.

#방법 1: 비공개 싱글턴 (Private Singleton)

목표: AudioPlayer 인스턴스를 하나만 유지하고, 다른 스크립트에서 직접 접근하지 못하도록 제한.

구현:

Awake에서 싱글턴 관리:

		ManageSingleton 메서드를 호출해 인스턴스 수를 체크.

		FindObjectsOfType<AudioPlayer>().Length로 현재 씬의 AudioPlayer 수 확인.

		인스턴스가 1개 초과면 현재 게임 오브젝트를 비활성화(gameObject.SetActive(false)) 후 파괴(Destroy(gameObject)).

		인스턴스가 하나뿐이면 DontDestroyOnLoad(gameObject)를 호출해 씬 전환 시에도 유지.
void Awake()
{
    ManageSingleton();
}

void ManageSingleton()
{
    int instanceCount = FindObjectsOfType<AudioPlayer>().Length;
    if (instanceCount > 1)
    {
        gameObject.SetActive(false);
        Destroy(gameObject);
    }
    else
    {
        instance = this;
        DontDestroyOnLoad(gameObject);
    }
}
비활성화 이유:

		오브젝트가 파괴되기 전에 다른 스크립트가 접근할 가능성을 차단.

		Unity의 실행 순서로 인해 드물게 발생할 수 있는 문제를 방지.

장점: 간단하고, AudioPlayer가 내부적으로만 관리되므로 외부 접근을 제한해 안전.

단점: 다른 스크립트에서 AudioPlayer를 참조하려면 명시적으로 찾아야 함.

#방법 2: 공개 싱글턴 (Public Singleton)

목표: AudioPlayer 인스턴스를 하나로 유지하면서 다른 스크립트에서 쉽게 접근 가능하도록 설정.

구현:

1. 정적 변수 추가:

		static AudioPlayer instance를 클래스 상단에 선언.

		static 키워드는 클래스의 모든 인스턴스가 공유하는 단일 변수를 만듦.

2. ManageSingleton 수정:

		FindObjectsOfType 대신 instance 변수를 체크.

		첫 번째 AudioPlayer면 instance = this로 설정하고 DontDestroyOnLoad 호출.

		이후 생성된 AudioPlayer는 비활성화 후 파괴.

3. 공개 접근 제공:

		public static AudioPlayer GetInstance() { return instance; } 메서드를 추가해 다른 스크립트에서 인스턴스에 접근 가능.
static AudioPlayer instance;

void ManageSingleton()
{
    if (instance != null)
    {
        gameObject.SetActive(false);
        Destroy(gameObject);
    }
    else
    {
        instance = this;
        DontDestroyOnLoad(gameObject);
    }
}

public static AudioPlayer GetInstance()
{
    return instance;
}
사용 예시:

		다른 스크립트에서 AudioPlayer.GetInstance().PlayShootingClip()처럼 호출 가능.

		이는 FindObjectOfType을 호출해 오브젝트를 찾는 과정을 생략.

장점:

		글로벌 접근이 쉬워 코드가 간결해짐.

		AudioPlayer를 씬에서 직접 찾을 필요 없음.

단점:

		공개 접근은 다른 스크립트에서 AudioPlayer를 무분별하게 사용할 가능성을 높임.

		대규모 프로젝트에서는 의존성 관리가 복잡해질 수 있음.

#4. Unity에서의 실행 결과

비공개 싱글턴:

		메인 메뉴에서 게임 씬으로 전환 시 AudioPlayer가 DontDestroyOnLoad에 추가되어 유지됨.

		음악이 끊기지 않고 연속 재생.

		Hierarchy에서 AudioPlayer가 DontDestroyOnLoad 아래에 표시됨.

공개 싱글턴:

		다른 스크립트에서 AudioPlayer.GetInstance()로 쉽게 접근 가능.

		하지만 글로벌 접근으로 인해 프로젝트가 커질수록 관리 어려움.

#5. 싱글턴 패턴의 주의점

장점:

		오브젝트가 하나만 존재하도록 보장.

		씬 전환 시 오브젝트 유지 가능.

단점:

		과도한 사용은 코드의 **결합도(Coupling)**를 높이고, 디버깅과 유지보수를 어렵게 만듦.

		공개 싱글턴은 다른 스크립트의 무분별한 접근으로 예기치 않은 문제를 일으킬 수 있음.

권장사항:

		AudioPlayer처럼 단순한 경우에 적합.

		대규모 프로젝트에서는 의존성 주입(Dependency Injection) 같은 대안을 고려.

#{ 싱글턴 패턴과 ScoreKeeper 개선 }

#1. 강의 목표

ScoreKeeper를 싱글턴(Singleton) 패턴으로 전환하여 여러 씬(Scene) 간 점수 데이터를 유지하고 관리.

UIGameOver 스크립트를 작성해 게임 오버 화면에서 점수를 표시.

LevelManager에서 점수 초기화를 적절히 처리하여 게임 루프 완성.

#2. 주요 개념: 싱글턴 패턴

싱글턴이란?

		싱글턴은 특정 클래스의 인스턴스가 오직 하나만 존재하도록 보장하는 디자인 패턴입니다.

		유니티에서는 씬 전환 시 객체가 유지되도록 DontDestroyOnLoad를 사용해 구현.

		예: AudioPlayer를 싱글턴으로 만들어 씬이 바뀌어도 음악이 끊기지 않게 함.

		장점: 전역 접근 가능, 중복 객체 방지.

		단점: 과도한 사용 시 코드 복잡성 증가, 의존성 문제 발생 가능.

싱글턴의 적합성 판단

		모든 객체가 싱글턴으로 적합하지 않음. 예: LevelManager는 여러 씬에 존재해도 큰 문제를 일으키지 않으므로 싱글턴으로 만들 필요가 적음.

		ScoreKeeper는 점수 데이터가 모든 씬에서 일관되게 유지되어야 하므로 싱글턴에 적합.

#3. ScoreKeeper 싱글턴 구현

public class ScoreKeeper : MonoBehaviour
{
    int score = 0;
    static ScoreKeeper instance;

    void Awake()
    {
        ManageSingleton();
    }

    void ManageSingleton()
    {
        if (instance != null)
        {
            gameObject.SetActive(false);
            Destroy(gameObject);
        }
        else
        {
            instance = this;
            DontDestroyOnLoad(gameObject);
        }
    }

    public int GetScore() { return score; }

    public void ModifyScore(int value)
    {
        score += value;
        score = Mathf.Clamp(score, 0, int.MaxValue);
        Debug.Log("Score updated: " + score);
    }

    public void ResetScore() { score = 0; }
}
static ScoreKeeper instance: 클래스의 단일 인스턴스를 저장.

ManageSingleton: 새로운 ScoreKeeper가 생성될 때 기존 인스턴스가 있으면 새 객체를 파괴하고,

								 없으면 현재 객체를 유지하며 DontDestroyOnLoad로 씬 전환 시 파괴되지 않도록 설정.

GetScore, ModifyScore, ResetScore: 점수 관리 메서드.

#4. UIGameOver 스크립트 작성

목적: 게임 오버 화면에서 점수를 표시.
public class UIGameOver : MonoBehaviour
{
    [SerializeField] TextMeshProUGUI scoreText;
    ScoreKeeper scoreKeeper;

    void Awake()
    {
        scoreKeeper = FindAnyObjectByType<ScoreKeeper>();
    }

    void Start()
    {
        scoreText.text = "You Scored:\n" + scoreKeeper.GetScore();
    }
}
설명

		TextMeshProUGUI: UI 텍스트를 표시하기 위한 컴포넌트.

		Awake: ScoreKeeper를 찾아 참조 저장.

		Start: 점수를 텍스트로 표시. \n은 줄바꿈을 위해 사용.

		문자열 연결: GetScore()는 정수를 반환하지만, 문자열과 연결 시 자동으로 문자열로 변환됨.

#5. LevelManager 수정

문제점: 게임 재시작 시 점수가 초기화되지 않음.

해결: LoadGame에서 ResetScore 호출.
public class LevelManager : MonoBehaviour
{
    [SerializeField] float sceneDelay = 2f;
    ScoreKeeper scoreKeeper;

    void Awake()
    {
        scoreKeeper = FindObjectOfType<ScoreKeeper>();
    }

    public void LoadGame()
    {
        scoreKeeper.ResetScore();
        SceneManager.LoadScene("Game");
        AudioPlayer audioPlayer = FindObjectOfType<AudioPlayer>();
        if (audioPlayer != null)
        {
            audioPlayer.PlayGameMusic();
        }
    }
}
설명

Awake: ScoreKeeper를 찾아 참조 저장.

LoadGame: 씬 로드 전에 ResetScore를 호출해 점수 초기화.

개념: FindObjectOfType은 씬에서 컴포넌트를 찾지만,

싱글턴 패턴을 사용하면 전역 접근이 가능해 코드가 간소화될 수 있음. 하지만 여기서는 명시적 참조를 유지.

#6. 왜 LevelManager는 싱글턴으로 만들지 않았나?

이유

		여러 LevelManager가 존재해도 게임 로직에 큰 영향을 미치지 않음.

		버튼 등 UI 요소가 특정 씬의 LevelManager를 참조하므로, 싱글턴으로 만들면 참조 문제가 발생할 수 있음.

		프리팹 설정으로 sceneDelay 등을 일관되게 관리 가능.

결론: LevelManager는 현재 구조로 충분히 작동하므로 싱글턴 전환 불필요.

#7. 추가 팁

싱글턴 사용 시 주의점

		싱글턴은 편리하지만, 과도하게 사용하면 코드 의존성이 높아져 유지보수가 어려워질 수 있음.

		대안으로 이벤트 시스템이나 매니저 클래스를 고려할 수 있음.


작동 흐름

		1. 게임 시작 → ScoreKeeper 싱글턴 인스턴스 생성, 점수 초기화.

		2. 게임 플레이 → ModifyScore로 점수 증가.

		3. 게임 오버 → UIGameOver가 ScoreKeeper에서 점수를 가져와 표시.

		4. 재시작 → LevelManager가 ResetScore 호출 후 새 게임 씬 로드.
Unity 게임 스크린샷
캔버스에 UI GameOver 스크립트 추가 캔버스의 스코어 텍스트 넣어주기

#{ 버그 수정, 밸런싱, 플레이 테스트 및 빌드 }

#1. 플레이 테스트 및 밸런싱

모든 씬을 점검하고, 게임의 재미와 난이도를 조정하는 단계.

2.1. MainMenu 씬 점검

		상태: 기본적으로 잘 작동.


		문제점:

				Quit 버튼: WebGL 플랫폼에서는 Quit 기능이 작동하지 않으므로, WebGL 빌드 시 제거 필요.

				크레딧 메뉴 부족: 시작 화면에 크레딧 버튼을 추가해 자산 제공자와 프로젝트 기여자에게 감사를 표할 수 있음.


		개선 방법:

				Button Group 아래에 크레딧 버튼 추가.

				새 캔버스를 만들어 기존 캔버스 위에 오버레이로 크레딧 화면 구성.



		개념: 플랫폼별 호환성을 고려해야 함. WebGL은 브라우저 기반이므로 Quit 같은 네이티브 기능이 제한됨.

		UI 설계 시 캔버스 오버레이는 메뉴 전환을 부드럽게 처리하는 방법.

2.2. GameOver 씬 점검

		상태: 기본 기능은 정상 작동.


		고려 사항: Canvas Scaler 설정.

				문제: Reference Resolution이 9:16 비율로 설정되어 있지만, 플레이어가 16:9 화면에서 플레이하면 UI(예: MainMenu 버튼)가 잘릴 수 있음.


				해결 옵션:

				높이에 맞춤: 화면 양옆을 확장해 모든 UI가 보이도록 설정.

				프로젝트 설정 변경: 빌드 설정에서 해상도 조정.

				권장: 빌드 전 Canvas Scaler 설정을 점검해 UI가 다양한 화면 비율에서 잘 보이도록 조정.


		개념: Canvas Scaler는 UI 요소의 크기와 위치를 다양한 해상도에 맞게 조정하는 Unity의 도구.

		Reference Resolution과 Scale With Screen Size 설정을 통해 UI 호환성을 보장.

2.3. Game 씬 점검

		플레이어:

				설정 조정:

						Player (Script)에서 이동 속도와 화면 제약(top 패딩값) 조정, 플레이어가 화면 하단 반 정도에서만 움직이도록 제한.

						Circle Collider의 Offset과 Radius를 조정해 충돌 감지 개선.

						Health와 Shooter는 현재 적절한 상태로 유지.


						개념: 밸런싱은 플레이어의 조작감과 난이도를 조정하는 과정. Collider는 물리적 충돌을 계산하므로,

						크기와 위치를 조정해 게임의 자연스러운 느낌을 개선.


		적군:

				새로운 적군 추가: 기존 적군 복제 후 에너지와 점수 조정(Enemy 1은 낮은 점수와 에너지).

				Shooter (Script)에서 발사 패턴 파라미터 수정.


				개념: 적군의 난이도와 보상(점수)은 게임의 재미와 도전 요소를 결정. 파라미터 튜닝은 플레이 테스트를 통해 최적화.


		Waves와 Paths:

				다양한 패턴의 Wave와 Path 추가.

				EnemySpawner에 새로운 Wave 추가로 다양성 확보.

				개념: Wave 시스템은 적군의 출현 패턴을 관리. 다양한 패턴은 게임의 반복성을 줄이고 플레이어의 흥미를 유지.


		AudioPlayer:

				음악과 효과음 볼륨 조정(과도하지 않도록).

				발사음 빈도가 높을 경우, 귀에 거슬리지 않도록 조정.

				개념: 오디오 밸런싱은 게임의 몰입감을 높이고, 과도한 소음으로 인한 피로를 방지.

#2. 게임 빌드 및 배포

#2.1. 빌드 설정

경로: File > Build Settings.

씬 추가: 빌드에 포함할 씬을 추가.

플랫폼 선택: PC, Mac, Linux, WebGL 등. 초기 설치에 따라 선택 가능한 플랫폼이 달라짐.

개념: Build Settings는 게임을 실행 가능한 파일로 변환하는 설정 창. 플랫폼별 호환성을 고려해 설정.

#2.2. 플레이어 설정 (Player Settings)

Resolution and Presentation:

		Fullscreen Mode: 전체 화면 또는 창 모드 선택.

		Default Screen Width/Height: 특정 해상도 강제 설정 가능.

		WebGL 빌드 시 기본 캔버스 해상도 조정(예: 9:16 비율로 540x960).

		Compression Format: Disabled로 설정해 공유 플랫폼 호환성 확보.

개념: Player Settings는 빌드된 게임의 실행 환경을 정의. 해상도 설정은 다양한 디바이스에서 일관된 경험을 제공.

#2.3. 빌드 과정

폴더 생성: [Builds] 폴더와 플랫폼별 하위 폴더(예: [Standalone], [WebGL]) 생성.

빌드 실행: Build 버튼 클릭 후 대상 폴더 선택.

소요 시간: 프로젝트 크기에 따라 몇 초에서 몇 시간 소요.

개념: 빌드는 게임을 실행 가능한 형태로 변환. WebGL은 브라우저 실행을 위해 압축 및 최적화 필요.

#2.4. 게임 배포

플랫폼: Sharemygame.com에서 게임 업로드.

업로드 과정:

		1. 계정 로그인/회원가입.

		2. Upload a Game 선택 후 WebGL 폴더 업로드.

		3. 업로드 완료 후 게임 테스트 및 피드백 확인.

		4. 수정 및 재업로드: 문제가 있으면 프로젝트 수정 후 새 버전 업로드.

개념: 게임 배포는 커뮤니티와 게임을 공유하는 단계. 피드백을 받아 개선점을 도출.
Unity 게임 스크린샷
화면의 높이에 맞추면 모든 화면의

양옆을 확장 옵션
Unity 게임 스크린샷
패딩을 수정해서 플레이어 캐릭터 이동 범위 화면의 반 정도로 제약
Unity 게임 스크린샷
빌드 셋팅
Unity 게임 스크린샷
빌드에서 플레이어 셋팅으로 들어가서 화면 크기를 고정하는 옵션을 선택

( with, height 를 반대로 적어놨는데 9:16비율로 게임을 만들었으니 거기에 맞춰서 값을 잘 넣어줘야한다 )
Unity 게임 스크린샷
빌드 진행 -> 빌드 완료
Unity 게임 스크린샷
WebGL 로 전환
Unity 게임 스크린샷
Resolution... 에서 해상도 조절

이후 Compression Format 을 Disabled 로 변경

#Netlify에 Unity WebGL 프로젝트를 배포

#1단계: Unity WebGL 빌드 준비

Unity에서 빌드하기

1. Unity에서 File > Build Settings 선택

2. Platform을 WebGL로 변경

3. Player Settings에서 다음 설정 확인:

		Publishing Settings > Compression Format: Gzip 또는 Brotli

		Resolution and Presentation > WebGL Template: Default 또는 Minimal

4. Build 버튼 클릭하여 빌드 폴더 생성

빌드 폴더 구조 확인

빌드 완료 후 다음과 같은 파일들이 생성됩니다:
MyGame/
├── Build/
│   ├── MyGame.data
│   ├── MyGame.framework.js
│   ├── MyGame.loader.js
│   └── MyGame.wasm
├── TemplateData/
│   └── (CSS, 이미지 파일들)
└── index.html

#2단계: Netlify 계정 생성 및 설정

1. Netlify 웹사이트 접속

		https://netlify.com 방문

		Sign up 클릭하여 계정 생성 (GitHub/GitLab/Email 선택 가능)

2. 대시보드 접속

		로그인 후 Netlify 대시보드로 이동

#3단계: 프로젝트 배포하기

방법 1: 드래그 앤 드롭 (가장 간단)

1. 파일 압축

		Unity 빌드 폴더 전체를 ZIP 파일로 압축

		또는 빌드 폴더 내의 모든 파일과 폴더를 선택

2. Netlify에 업로드

		Netlify 대시보드에서 Add new site > Deploy manually 선택

		또는 페이지 하단의 "Want to deploy a new site without connecting to Git? Drag and drop your site output folder here" 영역 사용

		ZIP 파일을 드래그해서 해당 영역에 드롭

		또는 폴더를 직접 드래그 앤 드롭

3. 배포 대기

		파일 업로드 및 배포 과정이 자동으로 진행됩니다

		보통 1-2분 정도 소요

#UIGameOver에서 ScoreKeeper를 찾는 시점을 Awake 대신 Start로 옮겨 문제 해결

스코어 키퍼에 문제가 생겨 스코어가 Awake 에서 초기화 되지 못해 UIGameOver 에서 0 으로 나오는 버그가 있었다

Awake 에서 start 로 초기화 코드를 옮겨주니 해결되었다 이는 WebGL 의 문제였다
UIGameOver가 Awake에서 ScoreKeeper를 찾을 때, WebGL의 느린 씬 로드 타이밍 때문에

ScoreKeeper가 아직 초기화되지 않아 null이 반환되거나 스코어가 0으로 표시됨.

#해결 방법 및 이유

방법 1: Awake → Start로 변경

		왜? Start는 모든 오브젝트가 초기화된 후 호출되므로 ScoreKeeper를 더 안정적으로 찾음.

		효과: WebGL의 불규칙한 로드 타이밍 문제를 피함.

방법 2: 이벤트 기반 접근법

		왜? ScoreKeeper의 스코어 변경 시 이벤트를 통해 UIGameOver가 자동으로 업데이트되므로 타이밍에 의존하지 않음.

		효과: 스코어가 실시간으로 반영되어 안정적.